package cn.miju.ser.unionpay.service;

import cn.miju.common.content.CodeMessage;
import cn.miju.common.util.ValidationUtil;
import cn.miju.database.constant.DbConstant;
import cn.miju.database.entity.PayOrder;
import cn.miju.database.entity.RefundOrder;
import cn.miju.database.service.PayOrderDataService;
import cn.miju.database.service.RefundOrderDataService;
import cn.miju.ser.common.rocketmq.DelayLevel;
import cn.miju.ser.common.rocketmq.Topic;
import cn.miju.ser.common.service.CommonService;
import cn.miju.ser.framework.constant.SerConstant;
import cn.miju.ser.framework.exception.ApiException;
import cn.miju.ser.framework.handler.MiJuContext;
import cn.miju.ser.framework.handler.MiJuContextHandler;
import cn.miju.ser.unionpay.vo.*;
import cn.miju.unionpay.handler.UnionPayApplyQrCodeHandler;
import cn.miju.unionpay.handler.UnionPayQueryOrderHandler;
import cn.miju.unionpay.handler.UnionPayRefundOrderHandler;
import cn.miju.unionpay.request.UnionPayApplyQrCodeReq;
import cn.miju.unionpay.request.UnionPayQueryOrderReq;
import cn.miju.unionpay.request.UnionPayRefundOrderReq;
import cn.miju.unionpay.response.UnionPayApplyQrCodeResp;
import cn.miju.unionpay.response.UnionPayQueryOrderResp;
import cn.miju.unionpay.response.UnionPayRefundOrderResp;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.Duration;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Objects;

/**
 * <pre>
 * 平台银联云闪付接口实现
 * </pre>
 *
 * @author zhuming
 * @version 1.0.0
 * @since 2023-03-14 11:29
 */
@Service
@RequiredArgsConstructor
public class UnionPayServiceImpl implements UnionPayService {
    private final UnionPayApplyQrCodeHandler applyQrCodeHandler;
    private final UnionPayQueryOrderHandler queryOrderHandler;
    private final UnionPayRefundOrderHandler refundOrderHandler;

    private final PayOrderDataService payOrderDataService;
    private final RefundOrderDataService refundOrderDataService;
    private final RocketMQTemplate rocketMQTemplate;
    private final StringRedisTemplate redisTemplate;
    private final CommonService commonService;

    /**
     * 支付结果通知地址
     */
    @Value("${miju.unionpay-pay-notify-url:}")
    private String payNotifyUrl;
    /**
     * 退款结果通知地址
     */
    @Value("${miju.unionpay-refund-notify-url:}")
    private String refundNotifyUrl;
    /**
     * 订单超时消息队列延迟等级
     */
    @Value("${miju.mq-delay-level}")
    private DelayLevel delayLevel;

    /**
     * 扫码支付下单
     * @param inVO {@link UpCreateQrcodeOrderInVO}
     * @return {@link UpCreateQrcodeOrderOutVO}
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public UpCreateQrcodeOrderOutVO createQrcodeOrder(UpCreateQrcodeOrderInVO inVO) {
        ValidationUtil.validate(inVO);
        MiJuContext miJuContext = MiJuContextHandler.getMiJuContext();
        PayOrder payOrder = payOrderDataService.getOne(new LambdaQueryWrapper<PayOrder>().
                eq(PayOrder::getOutTradeNo, inVO.getOutTradeNo()).
                eq(PayOrder::getPayAmount, inVO.getTotalAmount()).
                eq(PayOrder::getPayWay, DbConstant.PayWay.UNIONPAY).
                in(PayOrder::getStatus, DbConstant.PayOrderStatus.WAITING, DbConstant.PayOrderStatus.TIMEOUT).
                orderByAsc(PayOrder::getCreatedOrderTime).
                last(" LIMIT 1 "));
        // 针对重复下单二维码复用
        if (Objects.nonNull(payOrder) && Boolean.TRUE.equals(redisTemplate.hasKey(SerConstant.Redis.SCAN_CODE_PAY_QRCODE + payOrder.getPayWay() + ":" + payOrder.getPlatTradeNo()))) {
            String qrcode = redisTemplate.opsForValue().get(SerConstant.Redis.SCAN_CODE_PAY_QRCODE + payOrder.getPayWay() + ":" + payOrder.getPlatTradeNo());
            return UpCreateQrcodeOrderOutVO.builder().qrcode(qrcode).build();
        }
        String platTradeNo = commonService.getPlatTradeNo();
        LocalDateTime createdOrderTime = LocalDateTime.now();
        payOrder = PayOrder.builder().
                merchantId(miJuContext.getMerchantId()).
                outTradeNo(inVO.getOutTradeNo()).
                platTradeNo(platTradeNo).
                createdOrderTime(createdOrderTime).
                payAmount(inVO.getTotalAmount()).
                payWay(DbConstant.PayWay.UNIONPAY).
                payScen(DbConstant.PayScen.SCAN_CODE).
                build();
        payOrderDataService.save(payOrder);
        UnionPayApplyQrCodeResp resp = applyQrCodeHandler.doPlan(UnionPayApplyQrCodeReq.builder().
                outTradeNo(payOrder.getPlatTradeNo()).
                totalAmount(payOrder.getPayAmount()).
                title(inVO.getTitle()).
                description(inVO.getDescription()).
                notifyUrl(payNotifyUrl).
                timeExpire(inVO.getTimeExpire()).
                optionalReqParams(inVO.getOptionalReqParams()).
                optionalRespParams(inVO.getOptionalRespParams()).
                build());
        if (!resp.isSuccess()) {
            throw new ApiException(CodeMessage.FAIL.getCode(), resp.getMsg());
        }
        redisTemplate.opsForValue().set(SerConstant.Redis.SCAN_CODE_PAY_QRCODE + payOrder.getPayWay() + ":" + payOrder.getPlatTradeNo(), resp.getQrcode(), Duration.ofSeconds((inVO.getTimeExpire() == null || inVO.getTimeExpire() > 86400) ? 86400 : inVO.getTimeExpire()));
        rocketMQTemplate.syncSend(Topic.TIMEOUT_PAY_ORDER_TOPIC, MessageBuilder.withPayload(payOrder.getId()).build(), 3000, delayLevel.getLevel());
        return UpCreateQrcodeOrderOutVO.builder().
                qrcode(resp.getQrcode()).
                optionalRespParams(resp.getOptionalRespParams()).
                build();
    }

    /**
     * 查询订单
     * @param inVO {@link UpQueryOrderInVO}
     * @return {@link UpQueryOrderOutVO}
     */
    @Override
    public UpQueryOrderOutVO queryOrder(UpQueryOrderInVO inVO) {
        if (StringUtils.isBlank(inVO.getOutTradeNo()) && StringUtils.isBlank(inVO.getTradeNo())) {
            throw new ApiException(CodeMessage.VALIDATE_FAIL.getCode(), "传入商户订单号和服务商交易号不能都为空");
        }
        MiJuContext miJuContext = MiJuContextHandler.getMiJuContext();
        PayOrder payOrder = null;
        if (StringUtils.isNotBlank(inVO.getOutTradeNo())) {
            payOrder = payOrderDataService.getOne(new LambdaQueryWrapper<PayOrder>().eq(PayOrder::getOutTradeNo, inVO.getOutTradeNo()));
            if (Objects.isNull(payOrder)) {
                throw new ApiException("订单不存在");
            }
        }
        if (Objects.isNull(payOrder) && StringUtils.isNotBlank(inVO.getTradeNo())) {
            LambdaQueryWrapper<PayOrder> queryWrapper = new LambdaQueryWrapper<>();
            if (miJuContext.isToMerchantTradeNo()) {
                queryWrapper.eq(PayOrder::getTradeNo, inVO.getTradeNo());
            } else {
                queryWrapper.eq(PayOrder::getPlatTradeNo, inVO.getTradeNo());
            }
            payOrder = payOrderDataService.getOne(queryWrapper);
            if (Objects.isNull(payOrder)) {
                throw new ApiException("订单不存在");
            }
        }
        UnionPayQueryOrderResp resp = queryOrderHandler.doPlan(UnionPayQueryOrderReq.builder().
                outTradeNo(payOrder.getPlatTradeNo()).
                build());
        if (!resp.isSuccess()) {
            throw new ApiException(CodeMessage.FAIL.getCode(), resp.getMsg());
        }
        String tradeStatus;
        switch (resp.getTradeStatus()) {
            case "SUCCESS":
                if (DbConstant.PayOrderStatus.WAITING.equals(payOrder.getStatus())) {
                    payOrder.setStatus(DbConstant.PayOrderStatus.SUCCESS);
                    payOrder.setTradeNo(resp.getTradeNo());
                    payOrder.setUpdatedTime(LocalDateTime.now());
                    payOrderDataService.updateById(payOrder);
                }
                tradeStatus = DbConstant.PayOrderStatus.SUCCESS;
                break;
            case "NOT_EXIT":
                tradeStatus = DbConstant.PayOrderStatus.WAITING;
                break;
            case "REFUND":
                tradeStatus = DbConstant.PayOrderStatus.ALL_REFUND;
                break;
            case "CANCEL":
                tradeStatus = DbConstant.PayOrderStatus.CANCEL;
                break;
            default:
                tradeStatus = DbConstant.PayOrderStatus.FAIL;
                break;
        }
        return UpQueryOrderOutVO.builder().
                outTradeNo(payOrder.getOutTradeNo()).
                tradeNo(miJuContext.isToMerchantTradeNo() ? resp.getTradeNo() : payOrder.getPlatTradeNo()).
                totalAmount(resp.getTotalAmount()).
                tradeStatus(tradeStatus).
                build();
    }

    /**
     * 订单退款
     * @param inVO {@link UpRefundOrderInVO}
     * @return {@link UpRefundOrderOutVO}
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public UpRefundOrderOutVO refundOrder(UpRefundOrderInVO inVO) {
        ValidationUtil.validate(inVO);
        if (StringUtils.isBlank(inVO.getOutTradeNo()) && StringUtils.isBlank(inVO.getTradeNo())) {
            throw new ApiException(CodeMessage.VALIDATE_FAIL.getCode(), "传入商户订单号和服务商交易号不能都为空");
        }
        MiJuContext miJuContext = MiJuContextHandler.getMiJuContext();
        LambdaQueryWrapper<PayOrder> queryWrapper = new LambdaQueryWrapper<>();
        if (StringUtils.isNotBlank(inVO.getOutTradeNo())) {
            queryWrapper.eq(PayOrder::getOutTradeNo, inVO.getOutTradeNo());
        }
        if (StringUtils.isNotBlank(inVO.getTradeNo())) {
            if (miJuContext.isToMerchantTradeNo()) {
                queryWrapper.eq(PayOrder::getTradeNo, inVO.getTradeNo());
            } else {
                queryWrapper.eq(PayOrder::getPlatTradeNo, inVO.getTradeNo());
            }
        }
        PayOrder payOrder = payOrderDataService.getOne(queryWrapper);
        if (Objects.isNull(payOrder)) {
            throw new ApiException("订单不存在");
        }
        String platRefundNo = commonService.getPlatRefundNo();
        RefundOrder refundOrder = RefundOrder.builder().
                merchantId(miJuContext.getMerchantId()).
                payOrderId(payOrder.getId()).
                outRefundNo(inVO.getOutRefundNo()).
                platRefundNo(platRefundNo).
                refundAmount(inVO.getRefundAmount()).
                build();
        refundOrderDataService.save(refundOrder);
        UnionPayRefundOrderResp resp = refundOrderHandler.doPlan(UnionPayRefundOrderReq.builder().
                outRefundNo(platRefundNo).
                refundAmount(inVO.getRefundAmount()).
                outTradeNo(payOrder.getPlatTradeNo()).
                tradeNo(payOrder.getTradeNo()).
                payCreateTime(payOrder.getCreatedOrderTime().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"))).
                backUrl(refundNotifyUrl).
                build());
        refundOrder.setUpdatedTime(LocalDateTime.now());
        if (!resp.isSuccess()) {
            // 退款交易受理失败即表示退款失败
            refundOrder.setStatus(DbConstant.RefundOrderStatus.FAIL);
            refundOrder.setRefundResult(resp.getMsg());
            refundOrderDataService.updateById(refundOrder);
            return UpRefundOrderOutVO.builder().build();
        }
        refundOrder.setStatus(DbConstant.RefundOrderStatus.PROCESSING);
        refundOrder.setRefundResult("退款受理中");
        refundOrder.setTradeNo(resp.getTradeNo());
        refundOrder.setRefundTime(LocalDateTime.now());
        refundOrderDataService.updateById(refundOrder);
        String status = DbConstant.PayOrderStatus.PART_REFUND;
        int totalRefundedAmount = payOrder.getRefundedAmount() + refundOrder.getRefundAmount();
        if (totalRefundedAmount >= payOrder.getPayAmount()) {
            status = DbConstant.PayOrderStatus.ALL_REFUND;
            totalRefundedAmount = payOrder.getPayAmount();
        }
        payOrder.setStatus(status);
        payOrder.setRefundedAmount(totalRefundedAmount);
        payOrder.setUpdatedTime(LocalDateTime.now());
        payOrderDataService.updateById(payOrder);
        return UpRefundOrderOutVO.builder().
                tradeNo(miJuContext.isToMerchantTradeNo() ? refundOrder.getTradeNo() : refundOrder.getPlatRefundNo()).
                build();
    }
}
